Skip to main content
Declare your state, hand Ablo your DATABASE_URL, read it, and make one protected write. Your database is the system of record — Ablo never hosts your data. Ablo is the transaction layer on top of it: the client registers your Postgres connection once, every write commits there behind row-level security, and the confirmed rows fan out live to every connected client.
The schema is the API. Before you can read or commit anything — over the client SDK, the HTTP API, or an agent — you must define a schema and push it (ablo push). Your models become the API surface: a task model is what makes /v1/models/task exist. An API key authenticates you; the schema defines what you can call. With no pushed schema there is nothing to transact against, and your generated API Reference reflects your pushed models.

1. Install

npm install @abloatai/ablo

2. Declare a Schema

import Ablo from '@abloatai/ablo';
import { defineSchema, model, z } from '@abloatai/ablo/schema';

const schema = defineSchema({
  tasks: model({
    title: z.string(),
    status: z.enum(['todo', 'doing', 'done']),
  }),
});

export const ablo = Ablo({
  schema,
  apiKey: process.env.ABLO_API_KEY,
  databaseUrl: process.env.DATABASE_URL, // your Postgres — rows live here, never with Ablo
});
Customer apps should always pass schema. Treat it like Prisma’s schema file: it is the source of truth for typed model resources, realtime subscriptions, and agent writes. databaseUrl is server-only (the SDK throws if it sees one in a browser) and is sent once over TLS to register the connection — it is stored sealed and never echoed back. Use a dedicated non-superuser role: Ablo enforces tenant isolation with row-level security and rejects superuser or BYPASSRLS roles.

3. Push Your Schema

Declaring the schema in your code is not enough — the server holds its own copy. The sync server imports no app schema; it learns your models only from what you push, per tenant. Until you push, the server rejects every write to a model it hasn’t seen:
npx ablo push   # or `npx ablo dev` to push + watch during development
The server validates the schema, version-bumps it, and activates it. Only then do your models become writable over the wire.
Re-push after every schema change. Adding a model, or marking one mutable, changes nothing on the server until you push again. If a write fails with server_execute_unknown_model
Unknown model: "presentationSession". Mark the entity `mutable: true` in the
schema to expose it over the wire.
— the cause is almost always a schema you changed locally but never pushed. The fix is ablo push, not a code edit. (If the model is brand new it also needs a table — ablo migrate, or your existing migration tool — so the row has somewhere to land.)

4. Provision Your Tables

npx ablo migrate
This creates the synced-model tables (with row-level security) in your database and leaves every other table untouched. Your auth, billing, and anything not in the Ablo schema stay in your own ORM, owned by your own migrations.
Don’t want a connection string to leave your infrastructure? Keep DATABASE_URL in your app only and expose one signed Data Source endpoint instead — npx ablo init --storage endpoint scaffolds it from an ORM adapter (prismaDataSource / drizzleDataSource), and you omit databaseUrl from Ablo(...). Same product, same writes, same truth: your database is the system of record. The full shape is in Connect Your Database.

5. See it instantly

Once your schema is pushed and your tables exist, ablo init adds a small agent at ablo/agent.ts. Run it:
npx tsx ablo/agent.ts
It adds two tasks and moves the urgent one to the top. Keep the app open in another tab while it runs — the tasks appear there as the agent writes them, no refresh. To watch the changes go by in your terminal:
npx ablo logs
You and the agent are editing the same tasks, and you both see every change the moment it happens.

6. Read and Update

await ablo.ready();

const task = await ablo.tasks.retrieve({ id: 'task_123' });
if (!task) throw new Error('task not found');

const updated = await ablo.tasks.update({
  id: 'task_123',
  data: { status: 'done' },
});

console.log(updated.status);

7. Multiplayer and Coordination

There is no separate multiplayer mode. Use the same schema client for human UI, server actions, and agents; Ablo fans out confirmed writes and coordinates work on the same row. When a human or agent needs exclusive, ordered access to a row, take a claim. A claim is a disposable handle: it gives you the freshest row off claim.data, and auto-releases when it leaves scope (await using). Anyone else who claims the same row queues behind you.
await using claim = await ablo.tasks.claim({ id: 'task_123', action: 'update' });

// claim.data is the fresh row at the moment you acquired it
if (claim.data.status !== 'done') {
  await ablo.tasks.update({
    id: 'task_123',
    data: { status: 'done' },
    wait: 'confirmed',
  });
}
// claim auto-releases here as the block exits
The claim namespace exposes the rest of the coordination plane — each member takes an options object:
  • ablo.tasks.claim.state({ id }) — who currently holds the row, and the queue.
  • ablo.tasks.claim.queue({ id }) — the ordered list of waiters.
  • ablo.tasks.claim.release({ id }) — release a claim explicitly.
  • ablo.tasks.claim.reorder({ id, order }) — reprioritize the queue.

8. Next Steps

Keep using the schema client for app and agent writes. Reach for the advanced schema-less agent wrapper only when a worker intentionally cannot import the app schema.
  • CLI covers ablo init, schema push, and migrations.
  • Integration Guide explains the full app, React, multiplayer, and agent path.
  • Guarantees explains what confirmed writes and stale checks mean.
  • Client Behavior covers errors, retries, and public imports.
  • Connect Your Database covers both connection shapes — databaseUrl and the signed Data Source endpoint.
  • AI SDK Tool shows the same write path inside a tool call.
  • packages/sync-engine/examples/data-source contains the runnable signed Data Source contract demo.